Tester.php

<?php

namespace Tlf;

class Tester {

    use Assertions;
    use Tester\Databasing;
    use Tester\UtilityTrait;

    protected $benches;
    protected $catchers = [];
    /**
     * Comparisons from a single test. Should be reset between tests.
     */
    protected $comparisons = ['true' => 0, 'false'=>0];
    protected $disabled = false;

    protected $results = [];
    protected $options = [];

    /**
     * The cli class used to run the tests
     */
    public $cli = null;

    /**
     * @param $options usually args passed from the command line. 
     */
    public function __construct(array $options=[], $cli=null){
        $this->options = $options;
        $this->prepare();
        if ($this->options['set_error_handler']??true){
            set_error_handler([$this,'throwError']);
        }
        $this->cli = $cli;
    }
    public function throwError($errno, $errstr, $errfile, $errline) {
        throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
    }


    public function disable(){
        $this->disabled = true;
    }

    /**
     * Call `$tester->test('TestName')->compare($target,$actual)` to run a sub-test inside a test in your class
     * Chaining is not required
     * 
     * @export(Usage.test)
     */
    protected function test($subTestName){
        echo "\n\n****subtest: $subTestName****";
        return $this;
    }
    protected function startTest($key){
        echo "\n    ".'inner test \''.$key.'\' starts:';
    }
    protected function endTest($key,$result){
        echo "\n        ".'inner test \''.$key.'\' ends with result: <b>'.($result ? 'true' : 'false').'</b>';
    }

    /**
     * Get an array of method names on $this that start with 'test'
     */
    protected function getTestMethods(){
        $class = get_class($this);
        $check_class = $this->options['class']??'';
        if (isset($this->options['class'])
            &&substr($class,-strlen($check_class))!=$check_class
        ){
            return [];
        }
        $methods = get_class_methods($this);
        $methods = array_map(function($key,$value) //use ($builtInMethods)
        {
            if ($value=='test')return null;
            if (substr($value,0,4)!='test')return null;
            return $value;
        },array_keys($methods),array_values($methods));

        return $methods;
    }

    protected function startOb(){
        return \Tlf\Tester\Utility::startOb();
    }
    protected function endOb($ob_level){
        return \Tlf\Tester\Utility::endOb($ob_level);
    }


    /**
     * Run tests
     *
     * @param $methods an array of method names to run as tests or NULL to run all methods beginning with 'test'
     */
    public function run(?array $methods = null){
        if ($methods===null)$methods = $this->options['test'] ?? $this->getTestMethods();
        // if ($methods===null)$methods = $this->getTestMethods();
        //
        // var_dump($this->options['test']);
        
        $testsPassingCount=0;
        $testsFailingCount=0;
        $testsRunCount=0;
        // $this->results...
        foreach ($methods as $method){
            if ($method==NULL)continue;
            $this->inverted = false;
            if (!method_exists($this, $method)&&substr($method,0,4)!='test'){
                $method = 'test'.ucfirst($method);
            }
            if (!method_exists($this, $method)){
                continue;
            }

            $result = ['method'=>substr($method,4), 'error'=>null];

            $this->comparisons = ['true'=>0, 'false'=>0];
            $this->catchers = [];
            $error = null;
            $retValue = null;
            $this->benchStart('test_'.$method);
            $ob_level = $this->startOb();
            try {
                $testsRunCount++;
                $result['returnVal'] = $this->$method();
            } catch (\Throwable $t){
                $result['returnVal'] = null;
                $result['error'] = $t;
            } 
            $result['output'] = $this->endOb($ob_level);
            $result['bench'] = $this->benchEnd('test_'.$method);

            if ($result['error']!==null||$result['returnVal']===false||$this->comparisons['false']>0||$this->comparisons['true']==0){
                $result['result'] = false;
                $testsFailingCount++;
            } else {
                $testsPassingCount++;
                $result['result'] = true;
            }

            $result['disabled'] = $this->disabled;
            $this->disabled = false;
            if (($c=count($this->catchers))>0){
                // $result = false;
                $result['output'] = "---exception-fail---\n{$c} exceptions were not handled.\n----------\n".$result['output'];
            }
            $result['html'] = $this->htmlOutput((object)$result);
            $this->results[$method] = $result;
        }

        $this->results['_testsPassing'] = $testsPassingCount;
        $this->results['_testsFailing'] = $testsFailingCount;
        $this->results['_testsRun'] = $testsRunCount;
    
        return $this->results;
        //now do what we do with results.
    }
    public function benchStart($key){
        $this->benches[$key]['start'] = microtime(true);
    }
    public function benchEnd($key){
        $end = microtime(true);
        $start = $this->benches[$key]['start'];
        unset($this->benches[$key]);
        $diff = $end - $start;
        return (object)[
            'start'=>$start,
            'end'=>$end,
            'diff'=>$diff
        ];
    }

    //@export_start(Example.ModifyOutput)
    public function htmlOutput($details){
        ob_start();

        $successStatement = $details->result ? '<span style="color:green;">success</span>' : '<span style="color:red;">fail</span>';
        if ($details->error!=null)$successStatement = '<strong style="color:blue;">error</strong>';
        if ($details->disabled===true)$successStatement = '<strong style="color:orange;">disabled</strong>';
        $diff = $details->bench->diff;
        if ($diff < 0.0001)$diff = '';
        else $diff = 'in '.number_format($diff*1000,3).'ms';
        echo "<details>\n    <summary><b>".$details->method.":</b> ".$successStatement." {$diff}   </summary>\n";
            // echo "    <div>Time to run: ".$details->bench->diff."</div>";
            echo "    <div style='padding-left:4ch;white-space:pre;'>\n";
                $detailsOutput = htmlentities($details->output);
                $detailsLines = explode("\n",$detailsOutput);
                
                $detailsLines = array_map(function($value){return '        '.$value;},$detailsLines);
                echo implode("\n",$detailsLines);
                // var_dump($detailsLines);
            echo "\n    </div>";
            if ($details->error!=null){
                echo "\n    <br>\n";
                echo "    <div style='color:red;padding-left:4ch;white-space:pre;'>\n";
                    $errorOutput = $details->error;
                    $errorLines = explode("\n",$errorOutput);
                    $errorLines = array_map(function($value){return '        '.$value;},$errorLines);
                    echo implode("\n",$errorLines);
                echo "\n    </div>";
            }
        echo "\n</details>\n";

        return ob_get_clean();
    }
    //@export_end(Example.ModifyOutput)

    public function prepare(){

    }


    /**
     * On a class that extends `\Taeluf\Tester`, call `ExtendingClass::runAll()` to run the tests.
     * 
     * @deprecated this function no longer does anything.
     * @export(RunTests.All) 
     */
    static public function runAll(){
        // $tester = new static();
        // $tester->run();
        // return $tester;
    }

    /**
     * 
     * @deprecated this function no longer does anything
     */
    static public function runTests($name){
        // $tester = new static();
        // $tester->run($name);
        // return $tester;
    }

    /**
     * 
     * @deprecated this function no longer does anything
     */
    static public function runAllToFile($filePath,$andPrint = true){
        // ob_start();
        // $tester = static::runAll();
        // $output = ob_get_clean();
        // if ($andPrint)echo $output;
        // file_put_contents($filePath, $output);
//
        // return $tester;
    }
    /**
     * Call `Taeluf\Tester::xdotoolRefreshFirefox($switchBackToCurrWindow = false)` to refresh your browser tab.
     * If you're writing you're using `runAllToFile($file)`, this could come in handy.
     * 
     * @deprecated in favor of \Taeluf\Tester\Utility::xdotoolRefreshFirefox()
     * @export(Extra.RefreshBrowserTab)
     */
    static public function xdotoolRefreshFirefox($switchBackToWindow = false){
        $args = $switchBackToWindow ? ' y' : '';
        system(__DIR__.'/reload.sh'.$args);

        \Tlf\Tester\Utility::xdotoolRefreshFirefox($switchBackToWindow);
    }

}